从HashMap说开来
ConcurrentHashMap
整体思路是类似 HashMap 的,不过使用 CAS 和 synchronized 联合来保证线程安全。
CAS 操作:
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
不像 HashTable 对整个方法进行拿锁,以put方法为例: 当且仅当在往bucket中添加非第一个数据的时候,才会使用 synchronized 来进行拿锁。
其余情况使用 CAS 操作来保证线程安全的。CAS指令执行时,当且仅当V符合A时,处理器才会用B更新V的值,否则它就不执行更新。但是,不管是否更新了V的值,都会返回V的旧值。
HashTable
整体思路类似 HashMap 1.7 版本。与1.8 版本 HashMap 的区别:
- 没有使用红黑树进行数据处理
- 线程安全,不过是使用 synchronized 来修饰方法调用来保证
- 没有使用 二次 hash 来保证,高位也会参与到运算当中
- 没有使用 & 的操作来获取 index,而还是 % 取余的方式
- 扩容的时候
- 初始容量变成11,而不再是2的n此幂
- 扩容的时候,容量翻倍+1
- 插入数据的时候,使用头插法,而非尾插法。
HashSet
HashSet 内部包含一个 HashMap,其实现也是判断key是否多次出现,本质上用的就是 HashMap,value 都是一个固定的 Object 对象。
LinkedHashMap
继承于 HashMap ,基本上所有的关键方法都是用的 HashMap 来进行实现的,仅有部分方法是 hook 实现的。简单来说就是 HashMap 配合一个双向链表,来存放k-v的顺序。
其核心在于多了两个成员变量
static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {
LinkedHashMapEntry<K,V> before, after;
LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
/**
* 头部是最老的数据
*/
transient LinkedHashMapEntry<K,V> head;
/**
* 尾部是最新的数据
*/
transient LinkedHashMapEntry<K,V> tail;
核心代码
在关键位置进行hook:
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMapEntry<K,V> p =
new LinkedHashMapEntry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}
上篇中 我们分析了,在putVal的时候,会创建一个新的Node,用的就是 HashMap 的 newNode 方法。而这里重写了此方法,因此 putVal 会调用到内部。
private void linkNodeLast(LinkedHashMapEntry<K,V> p) {
LinkedHashMapEntry<K,V> last = tail;
tail = p;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
}
插入时,直接会进行尾插的方式,把新数据插到尾部。
同时hook了get方法,期间调用了方法:
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMapEntry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMapEntry<K,V> p =
(LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
将相应的数据移动到尾部。当然了,get 方法是由 accessOrder 来进行控制的。为 true 才会将get也移动元素。
其余就遍历的时候会用自己实现的方法来覆盖HashMap的方法:
public void forEach(BiConsumer<? super K, ? super V> action) {
if (action == null)
throw new NullPointerException();
int mc = modCount;
// Android-changed: Detect changes to modCount early.
for (LinkedHashMapEntry<K,V> e = head; modCount == mc && e != null; e = e.after)
action.accept(e.key, e.value);
if (modCount != mc)
throw new ConcurrentModificationException();
}
整个遍历是在遍历双向链表而非遍历底层的数组。
LruCache
其核心就是一个 LinkedHashMap,不过完全没有用 eldest 而已。
直接看方法:
get
public final V get(K key) {
// 不支持null 的key
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
//尝试获取相应的value,有的话就直接返回好了。
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
/*
* Attempt to create a value. This may take a long time, and the map
* may be different when create() returns. If a conflicting value was
* added to the map while create() was working, we leave that value in
* the map and release the created value.
*/
//如果没有,尝试让用户自己判断,要不要自己生成一个。
V createdValue = create(key);
if (createdValue == null) {
return null;
}
//如果确实自己生成了一个,就把它放进链表中
synchronized (this) {
createCount++;
mapValue = map.put(key, createdValue);
// 因为 create 时,可能别的地方已经放入了一个数据,因此需要二次判断是否放置位置有数据,
//如果有数据,那肯定是被别的线程捷足先登,因此要重新还原人家设置的数据
if (mapValue != null) {
// There was a conflict so undo that last put
map.put(key, mapValue);
} else {
//如果没有,那么直接扩容
size += safeSizeOf(key, createdValue);
}
}
if (mapValue != null) {
//如果用户自己create了一个对象,但是已经被别人捷足先登,那么给出一个回调,告诉用户,你那里的create 不好使了。
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
//缩容到最大限制。
trimToSize(maxSize);
return createdValue;
}
}
由于 create 方法可能会耗时好长。同时有别的线程来操作因此,要增加一个二次校验。
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
同时 entryRemoved 方法是一个空方法,如果没有实现就不会有任何显示。